feat: projects as backend sources with system prompt injection#8739
feat: projects as backend sources with system prompt injection#8739
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1eb462962d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const sources = (raw.sources ?? []) as SourceEntry[]; | ||
| return sources.map(toProjectInfo); |
There was a problem hiding this comment.
Filter archived projects from listProjects
listProjects() currently returns every project source without checking archivedAt, but the active project UIs consume this method via projectStore.fetchProjects(). After calling archiveProject(), archived entries stay in the normal project lists and remain selectable instead of moving out of active views. Filter out items with non-null archivedAt before returning active projects.
Useful? React with 👍 / 👎.
| preferredModel, | ||
| workingDirs, | ||
| useWorktrees, | ||
| order: 0, |
There was a problem hiding this comment.
Preserve existing project order during update
updateProject() rewrites project properties with order: 0 on every edit, which overwrites any order set by reorderProjects() and causes edited projects to jump in views that sort by order. The update path should carry forward the current order (or leave it untouched) rather than hard-resetting it.
Useful? React with 👍 / 👎.
| {filtered.map((skill) => ( | ||
| <div | ||
| key={skill.name} | ||
| key={`${skill.global ? "g" : "p"}-${skill.name}`} |
There was a problem hiding this comment.
Use unique keys for project-scoped skill rows
The row key only uses global + name, so two project-scoped skills from different projects with the same name produce duplicate React keys. This can cause row/menu state to be reused across items when includeProjectSources is enabled. Include a project discriminator (for example projectDir) in the key.
Useful? React with 👍 / 👎.
| "null" | ||
| ], | ||
| "description": "Absolute path to the project root. Required when `global` is false." | ||
| }, | ||
| "properties": { |
There was a problem hiding this comment.
Add projectId to CreateSourceRequest schema
The ACP schema entry for CreateSourceRequest was updated for properties but still omits the new projectId request field introduced in Rust. That leaves generated SDK clients/types unable to send projectId, so typed consumers cannot use project-scoped source creation through the new convenience path.
Useful? React with 👍 / 👎.
1eb4629 to
af6a5fc
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: af6a5fc794
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| type: "project", | ||
| }); | ||
| const sources = (raw.sources ?? []) as SourceEntry[]; | ||
| return sources.map(toProjectInfo); |
There was a problem hiding this comment.
Sort projects by persisted order before returning
listProjects() now returns sources.map(toProjectInfo) directly, but _goose/sources/list is name-sorted and does not honor properties.order. This means every refetch (startup, refresh, after CRUD) can reshuffle the Projects view away from the user’s drag-and-drop order even though reorderProjects() persists order values. Sort by order (with a stable tiebreaker) before returning to preserve the saved project ordering.
Useful? React with 👍 / 👎.
| /// When true, also scan the working directories of all known projects for | ||
| /// project-scoped sources (e.g. skills stored under `{workingDir}/.agents/skills/`). | ||
| #[serde(default)] | ||
| pub include_project_sources: bool, |
There was a problem hiding this comment.
Regenerate ACP schema for includeProjectSources
ListSourcesRequest now defines include_project_sources in Rust, but the ACP schema/generated SDK contract still omits this field, so typed clients cannot request project-scoped source discovery without unsafe casts. This creates a contract drift between server behavior and published client types; update/regenerate schema artifacts to include includeProjectSources.
Useful? React with 👍 / 👎.
Move projects from Tauri IPC commands to the ACP sources system. Backend: - Add Project to SourceType, properties bag on SourceEntry - Project storage in Paths::data_dir()/projects/ - System prompt injection in agent reply via load_project_instructions - _goose/session/set_project ACP method - includeProjectSources flag for listing skills from project working dirs - projectId convenience on CreateSourceRequest for project-scoped skills Frontend: - Rewrite projects API from Tauri invoke to ACP ext methods - Remove buildProjectSystemPrompt (backend handles it now) - Filter archived projects, sort by persisted order - updateProject preserves existing order - Add project picker to CreateSkillDialog - Show project badges on non-global skills - Fix SDK listSessions rename, TS strict errors Delete ui/goose2/src-tauri/src/commands/projects.rs (508 lines). Signed-off-by: Douwe Osinga <douwe@squareup.com>
af6a5fc to
dccd332
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: dccd332f90
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const all = await listProjects(); | ||
| return all.filter((p) => p.archivedAt !== null); |
There was a problem hiding this comment.
Read archived projects from an unfiltered source list
listArchivedProjects() now calls listProjects(), but listProjects() already drops every project with archivedAt !== null, so this function can only ever return an empty array. In ui/goose2/src/features/settings/ui/SettingsModal.tsx, archived project management is populated from listArchivedProjects(), so users cannot see or restore archived projects after archiving them.
Useful? React with 👍 / 👎.
| global: !options?.projectId, | ||
| ...(options?.projectId ? { projectId: options.projectId } : {}), |
There was a problem hiding this comment.
Resolve projectId before calling
_goose/sources/import
When importSkills() is called with options.projectId, the request sends global: false plus projectId, but the backend import contract only accepts projectDir for scoped imports (ImportSourcesRequest), so projectId is ignored and the server receives a project-scoped import with no projectDir. That path fails validation (projectDir is required when global is false), so project-scoped skill imports are broken for any caller that uses this new option.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c4ef532a9c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const raw = await client.extMethod("_goose/sources/update", { | ||
| type: "project", | ||
| name: existing.id, |
There was a problem hiding this comment.
Use supported update contract for project mutations
updateProject() is now wired to _goose/sources/update but sends { type, name, ... } without the required path, and the same payload shape is reused by archiveProject, restoreProject, and reorderProjects. The ACP UpdateSourceRequest requires path, and the backend update path still enforces skill-only updates, so these project edit/archive/reorder flows fail instead of persisting changes.
Useful? React with 👍 / 👎.
| await client.extMethod("_goose/sources/delete", { | ||
| type: "project", | ||
| name: id, |
There was a problem hiding this comment.
Call project delete with a valid backend identifier
deleteProject() calls _goose/sources/delete with { type, name, global }, but the delete contract requires a path field and the backend delete implementation is skill-path based. In practice this means project deletion requests fail at runtime, so users cannot remove projects from the UI.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 311fd33b3e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| } | ||
|
|
||
| fn projects_dir() -> PathBuf { | ||
| Paths::data_dir().join("projects") |
There was a problem hiding this comment.
Add legacy project migration/fallback before switching storage
This hard-switches project storage to Paths::data_dir()/projects and only reads .md sources from that directory, but the deleted desktop path stored existing user projects under ~/.goose/projects/*/project.json. Without a migration or fallback read path, upgraded users will see their existing projects disappear (and thread project_id values pointing to old project IDs will no longer resolve instructions), which is a data-visibility regression for all existing installs.
Useful? React with 👍 / 👎.
|
|
||
| useChatSessionStore.getState().updateSession(sessionId, { projectId }); | ||
|
|
||
| void setSessionProject(sessionId, projectId).catch(console.error); |
There was a problem hiding this comment.
Await project metadata update before relying on backend prompt
After removing frontend project prompt injection, backend thread metadata is now authoritative for project instructions, but project changes call setSessionProject in fire-and-forget mode (void ...catch(...)). If the user sends a message immediately (or the request is delayed/fails), the UI state shows the new project while the backend still uses the old project for prompt construction, so at least one reply can be generated with the wrong project context.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6d6cd3b98b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| await createSkill(name, description.trim(), instructions, | ||
| selectedProjectId !== "global" ? { projectId: selectedProjectId } : undefined, | ||
| ); |
There was a problem hiding this comment.
Wire project-scoped skill selection into createSkill
The new "Save to" control passes a 4th argument to createSkill, but the helper in features/skills/api/skills.ts still only accepts (name, description, instructions) and always sends global: true. That means this new argument is ignored at runtime, so choosing a project still creates a global skill and project-scoped creation from this dialog does not work.
Useful? React with 👍 / 👎.
| "saveLocation": "Save to", | ||
| "saving": "Saving...", | ||
| "scopeGlobal": "Global", | ||
| "saveLocationHint": "Global skills are available in all sessions. Project skills are stored in the project directory." |
There was a problem hiding this comment.
Add locale entries for new skill scope labels
This commit introduces new dialog keys for skill save scope, but only the English locale is updated. CreateSkillDialog now renders these keys, so non-English locales (for example locales/es/skills.json) will show fallback English/raw-key text for this new UI. Please add matching keys across supported locales when adding new i18n strings.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 690f564851
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| /// Read a single project source by name. | ||
| pub fn read_project(name: &str) -> Result<SourceEntry, Error> { | ||
| let dir = projects_dir(); | ||
| let file = dir.join(format!("{name}.md")); |
There was a problem hiding this comment.
Validate project IDs before resolving project file paths
read_project() directly interpolates name into a filesystem path ({name}.md) without validating or normalizing it, and project_id is accepted from session metadata/update methods as arbitrary user input. A value like ../../tmp/evil can escape the projects directory and cause unrelated markdown files to be read as project instructions (or working-dir metadata), so project IDs should be validated (e.g., same slug rules as project creation) before path construction.
Useful? React with 👍 / 👎.
| @@ -254,6 +254,7 @@ describe("CreateSkillDialog", () => { | |||
| "my-skill", | |||
| "A description", | |||
| "Some instructions", | |||
| { projectId: undefined }, | |||
There was a problem hiding this comment.
Fix dialog API call assertions to match actual arguments
This test now expects createSkill to receive { projectId: undefined }, but the dialog passes undefined for the 4th arg in the global-scope path; similarly, another updated assertion expects an extra 5th arg for updateSkill that is never sent. These mismatched expectations make the tests fail even when behavior is correct, creating a false-negative CI blocker.
Useful? React with 👍 / 👎.
| @@ -0,0 +1,3 @@ | |||
| { | |||
| "binaryPath": "/Users/douwe/proj/wt-goose-4/ui/goose-binary/goose-binary-darwin-arm64/bin/goose" | |||
There was a problem hiding this comment.
Drop machine-specific binary path from tracked config
This new file hardcodes a developer-local absolute path (/Users/douwe/...) into the repository. On other machines that path is invalid, and any workflow that consults this file will fail to locate the binary; even if currently unused, this is a non-portable local artifact that should not be committed.
Useful? React with 👍 / 👎.
…esign Merges main's path-based source addressing (#8731), redesigned skills library (#8868), and 70+ other commits while preserving the projects-as-sources work. Backend: - SourceType gains Project alongside main's Skill/BuiltinSkill/Recipe/Subrecipe/Agent - SourceEntry.directory renamed to .path so the field name matches its role (a stable path identifier passed back to update/delete/export, regardless of whether the underlying entity is a directory or file) - SourceEntry.properties (free-form metadata bag) coexists with main's supporting_files - ListSourcesRequest.include_project_sources scans every known project's workingDirs and tags resulting skills with projectName/projectDir - CreateSourceRequest.project_id resolves to the project's first workingDir - Projects stored as <dataDir>/projects/<slug>.md flat-file frontmatter - read_project / project_working_dirs helpers used by agent.rs to inject project instructions into the system prompt - Drops our SetSessionProjectRequest in favor of main's identical UpdateSessionProjectRequest (_goose/session/update_project) Frontend: - features/projects/api/projects.ts rewritten on the typed SDK; ProjectInfo loses createdAt/updatedAt/artifactsDir, gains path - Skills API uses main's typed GooseSourcesCreate/List/Update/Delete/Export/ Import calls. toSkillInfo prefers backend-provided properties.projectName/ projectDir tags over path-derived guesses - CreateSkillDialog keeps our save-location picker (Global vs each project) on top of main's redesigned UI; edits use path-based addressing - SkillsView takes main's redesigned filter row + sectioned list rendering - AppShell drops the old inline-type adapter - Removes ui/goose2/src-tauri/src/commands/projects.rs (508 lines) and its 9 Tauri command registrations; main's project_icons.rs (filesystem icon scanning, a real shell concern) is kept Verification: cargo test -p goose passes (incl. 17 sources tests, 2 new project ones); cargo clippy clean; pnpm tsc clean; pnpm test 538/538; pnpm check clean.
Pulls in 7 new commits from main: - #8932 break up acp/server.rs into submodules - #8924 custom providers UI/ACP - #8935 keep renamed skills open in detail view - #8897 skill refactor (CreateSkillDialog -> SkillEditor) - #8886 copy/content polish - #8905 Gemini OAuth fix - #8546 command injection patterns Conflicts resolved: - crates/goose/src/acp/server.rs: took main's split structure; added project-related plumbing (properties, project_id, include_project_sources) into crates/goose/src/acp/server/sources.rs. - update_thread_metadata: pub(super) so server/sessions.rs (which hosts on_update_session_project) can call it. - ui/goose2/src/features/skills/api/skills.ts: dropped directoryPath/editable (not on main's SkillInfo); kept our projectName/projectDir lookup from backend properties. - ui/goose2/src/features/skills/ui/SkillEditor.tsx (renamed from CreateSkillDialog.tsx in #8897): kept main's structure; re-added our save-location picker (Global vs each project) and onSaved callback. - ui/goose2/src/shared/i18n/locales/en/skills.json: took main's casing for editTitle/newTitle; kept our new keys (global, globalHint, projectHint, saveLocation). Bumped useChatSessionController.ts file-size limit by 10 lines (840 -> 850); justification updated to mention ACP project sync.
690f564 to
2115b76
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2115b76b1b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| path: source.path, | ||
| fileLocation: getSkillFileLocation(source.path), |
There was a problem hiding this comment.
Provide path fallback for source entries
This adapter now assumes every source has path, but existing ACP test fixtures still return directory (for example in ui/goose2/tests/e2e/fixtures/tauri-mock.ts), so source.path becomes undefined and downstream helpers like deriveProjectRoot() / getSkillFileLocation() crash on string operations. That makes mocked skills flows fail at runtime after this contract change; add a compatibility fallback when mapping (path or legacy directory) or update all fixtures in the same change.
Useful? React with 👍 / 👎.
| const id = slugify(name); | ||
| const raw = await client.extMethod("_goose/sources/create", { | ||
| type: "project", | ||
| name: id, |
There was a problem hiding this comment.
Disambiguate colliding project slugs on create
Project creation now uses slugify(name) directly as the persisted source ID, so different display names that normalize to the same slug (e.g. case/punctuation variants, or non-ASCII-only names collapsing to project) all send the same name to _goose/sources/create and the second create fails with "already exists." This is a user-facing regression in create flow; generate a unique slug (suffix strategy) before calling the backend.
Useful? React with 👍 / 👎.
Field renamed from 'directory' to 'path' in SourceEntry to reflect its role as the stable on-disk identifier (works for both directory- backed skills and flat-file projects). The e2e Tauri/ACP mock was still emitting 'directory:' for skill source entries, so the frontend's toSkillInfo() saw source.path === undefined and broke in deriveProjectRoot() / getSkillFileLocation(). Also drops a stale doc comment on UpdateSourceRequest.properties that described old replace-all semantics; keep the field self-documenting via its name.
Summary
Move projects from Tauri IPC commands to the ACP sources system, making them a backend-managed entity available to all clients (desktop, CLI, TUI). Project instructions are now injected into the system prompt by the agent (cacheable, once per conversation) instead of being prepended to every user message by the frontend.
Builds on #8675 (skills as sources over ACP) by adding
Projectas a second source type.Backend changes
Sources (
crates/goose/src/sources.rs)Projectvariant toSourceTypewith storage inPaths::data_dir()/projects/{name}.md.mdfiles with YAML frontmatter (name, description, plus arbitrarypropertieslike icon, color, workingDirs, preferredProvider)includeProjectSourcesflag tolist_sources()that scans all project working directories for project-scoped skillsprojectName/projectDirproperties for UI display (not persisted in the skill file)Agent (
crates/goose/src/agents/agent.rs)load_project_instructions()reads project source from disk via session → thread →project_id→ source filereply_internal()appends project instructions to the system prompt (not user message)ACP (
crates/goose-acp/src/server.rs,crates/goose-sdk/src/custom_requests.rs)_goose/session/set_projectmethod to update thread metadataproject_idproject_idfrom meta onnew_sessionprojectIdconvenience field onCreateSourceRequest(resolves project ID → working dir server-side)includeProjectSourcesflag onListSourcesRequestpropertiesfield onSourceEntry,CreateSourceRequest,UpdateSourceRequestFrontend changes
Projects API (
projects.ts)invoke()→ ACP_goose/sources/*ext method callsProjectInfomapped to/fromSourceEntrywith properties bagChat (
useChatSessionController.ts)buildProjectSystemPromptfrom prompt composition — backend handles this nowsetSessionProjectwhen project is assigned to a sessionprojectIdthrough session preparation chainSkills
CreateSkillDialog: add "Save to" dropdown (Global / Project X) for project-scoped skillsSkillsView: show project badge on non-global skillsprojectIdon create,includeProjectSourceson listDeleted
ui/goose2/src-tauri/src/commands/projects.rs(508 lines, 9 Tauri commands)buildProjectSystemPromptfunction and its testsHow it works